#!/usr/bin/env python
# -*- coding: utf-8 -*-	
#

## This is a filter to convert Visual Basic v6.0 or VB(A) code
# into something doxygen can understand.
# Copyright (C) 2005  Basti Grembowietz
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
# ------------------------------------------------------------------------- 
#
# This filter depends on following circumstances:
# in VB-code,
#  '! comments get converted to doxygen-class-comments (class and module comments)
#  '* comments get converted to doxygen-comments (function, sub, etc.)
#  all other comments are skipped
#

import getopt          # get command-line options
import os.path         # getting extension from file
import string          # string manipulation
import sys             # output and stuff
import re              # for regular expressions

## stream to write output to
outfile = sys.stdout
verbose = False

# regular expression
## non-Doxygen Comment lines
re_comments   = re.compile("([^\']*)\'.*")
re_VB_Name    = re.compile(r"\s*Attribute\s+VB_Name\s+=\s+\"(\w+)\"", re.I)

## module-level comments
re_doxy_class = re.compile("[^']*\'!(.*)")
## doxygen comments
re_doxy       = re.compile("[^']*\'\*(.*)")

## constant declarations
re_const    = re.compile(r"""
              \s*                                                       # Leading white space
              (Public\s+|Friend\s+|Private\s+|Static\s+){0,1}           # Optional Scope
              (Const)\s+                                                # const keyword
              (\w+)\s+                                                  # Variable name
              (As|=)\s+                                                 # assignment of value or type
              (\w*)\s+                                                  # Optional Data Type (String, Byte, etc)
              (=){0,1}                                                  # If 'As', then expect '='
              (.*)                                                      # assignment of value
              """, re.I | re.VERBOSE)

## re to search for class-members
re_members     = re.compile(r"""
                 \s*                                                    # Leading white space
                 (Public\s+|Friend\s+|Private\s+|Static\s+){0,1}        # Optional Scope
                 ([\w\(\)]+)\s+                                         # name of function and parameters in parens
                 As\s+(.*)                                              # Return type
                 """, re.I | re.VERBOSE)
re_assignments = re.compile(r"""
                 \s*                                                    # Leading white space
                 ([\w\(\)]+)                                            # At least 1 word
                 \s+=\s+                                                # =
                 (.*)                                                   # anything
                 """, re.I | re.VERBOSE)
re_sub         = re.compile(r"""
                 \s*                                                    # Leading white space
                 (Public\s+|Friend\s+|Private\s+|Static\s+){0,1}        # Optional Scope
                 (Sub|Property\s+Let|Property\s+Set)\s+                 # (Sub)Routine declaration
                 (\w+)\s*                                               # Routine name
                 (\([\w =,\(\)]*(.*)\))                                 # parameter list
                 """, re.I | re.VERBOSE)
re_function    = re.compile(r"""
                 \s*                                                    # Leading white space
                 (Public\s+|Friend\s+|Private\s+|Static\s+){0,1}        # Optional Scope
                 (Function|Property\s+Get)\s+                           # Function declaration
                 (\w*)\s*                                               # Function name
                 (\([\w =,\(\)]*\))                                     # parameter list
                 \s+As\s+(\w+|\W+)                                      # return type
                 """, re.I | re.VERBOSE)

## re to search for an external library (forward) declaration
re_declaration = re.compile(r"""
                 \s*                                                    # Leading white space
                 (.*)\s+                                                # Anything before 'Lib' - Optional Scope and the Declare keyword, etc.
                 Lib\s+                                                 #
                 (.*)                                                   # Library name and (optional) alias
                 \(\s*(.*)                                              # Everything else - Parameter list and return type
                 """, re.I | re.VERBOSE)

## re to search for enum or type
re_type     = re.compile(r"""
              \s*                                                       # Leading white space
              (Public\s+|Friend\s+|Private\s+|Static\s+){0,1}           # Optional Scope
              (Enum|Type)\s+                                            #
              (\w+)                                                     # Name of Type or enum
              """, re.I | re.VERBOSE)
re_endType  = re.compile(r"\s*End\s+(Enum|Type)(.*)", re.I)

#inheriting classes
re_inherit  = re.compile(r"""
              \s*                                                       # Leading white space
              Implements\s+                                             # 
              (\w+)                                                     # Name of Type or enum
              """, re.I | re.VERBOSE)

#list of inherited classes (if any)
inherited = None

## default "level" (private / public / protected) to take when not specified
def_level = "public:"

## strips vb-style comments from string
# @param str string to process
# @return str minus any regular VB comments it may have
def strip_comments(str):
	global re_comments
	my_match = re_comments.match(str)
	if my_match is not None:
		return my_match.group(1)
	else:
		return str

## Writes filename to stdout.
# @note
#  Used only if this script can't decide how to process the file.  
# See the method 'filter' at the bottom of this file for what can be processed.
# @param filename file to open and write
def dump(filename):
	f = open(filename)
	r = f.readlines()
	f.close()
	for s in r:
		sys.stdout.write("."), 
		sys.stdout.write(s)

## Transform Module or Class level comments into C++ style comments.
# Walks through the entire file looking for module level comments.
# All comments found are written out.
# @param r the list containing the source code to be processed
def processGlobalComments(r):
	global re_doxy_class
	for s in r:
		gcom = re_doxy_class.match(s)
		if gcom is not None:
			if (gcom.group(1) is not None):
				outfile.write("/// " + gcom.group(1) + "\n")

## Looks through entire file looking for Implements lines
#  representing base classes which need to be appended to the class name
#  in @ref processClassName
#
# @param r the list containing the source code to be processed
def findInheritedClasses(r):
    global re_inherit
    global inherited
    for s in r:
        cname = re_inherit.match(s)
        if cname is not None:
            if inherited is None:
                inherited = ": public " + cname.group(1)
            else:
                inherited = inherited + ", public " + cname.group(1)
    
## Method to use VB_Name Attribute as class or namespace name and
#  encapsulate all code either within a C++ class or namespace
# @param r the list containing the source code to be processed
# @param type string ("BAS" or "CLS" to identify whether the method should 
#        create a class or namespace
def processClassName(r, type):
	global re_VB_Name
	global verbose
	global inherited
	
	className = "dummy"
	if verbose:
		sys.stderr.write("Searching for classname... ") 
	for s in r:
		cname = re_VB_Name.match(strip_comments(s))
		if cname is not None:
			className = cname.group(1)
			if verbose:
				sys.stderr.write("found! ") 
			break
	if verbose:
		sys.stderr.write(" using " + className + "\n")
	else:
		sys.stderr.write("Processing " + className + "\n")
	if type is "CLS": 
	   if inherited is None:
	       outfile.write("\nclass " + className + "\n{\n")
	   else:
	       outfile.write("\nclass " + className + inherited + "\n{\n")
	else: 
		outfile.write("\nnamespace " + className + "\n{\n")

## Search a string for a standard doxygen style comment line
#  and write it if found.
# @param s a single line of source code 
def checkDoxyComment(s):
	global re_doxy
	doxy = re_doxy.match(s)
	if (doxy is not None):
		outfile.write("/// " + doxy.group(1) + "\n")

## Function to identify a member declaration in a string and write out a C++ equivalent
# @param s a single line of source code 
# @return True if the member regex is not empty
# @return False otherwise
def foundMember(s):
	global re_members
	member = re_members.match(strip_comments(s))
	if (member is not None):
		res_str = getAccessibility(member.group(1)) + " " + member.group(3) + " " + member.group(2)  + ";"
		outfile.write(res_str + "\n")
		return True
	else:
		return False

## Function to identify a function declaration in a string and write out a C++ equivalent
# @param s a single line of source code 
# @return True if the member regex is not empty
# @return False otherwise
def foundFunction(s):
	global re_function
	s_func = re_function.match(strip_comments(s))
	if (s_func is not None):
		res_str = getAccessibility(s_func.group(1)) + " " + s_func.group(5) + " " + s_func.group(3) + s_func.group(4) + ";"
		outfile.write(res_str + "\n")
		return True
	else:
		return False

## Function to identify a constant declaration in a string and write out a C++ equivalent
# @param s a single line of source code 
# @return True if the member regex is not empty
# @return False otherwise
def foundConst(s):
	global re_const
	s_const = re_const.match(strip_comments(s))
	if (s_const is not None):
		res_str = getAccessibility(s_const.group(1)) + " const " + s_const.group(5) + " " + s_const.group(3) + " = " + s_const.group(7) + ";"
		outfile.write(res_str + "\n")
		return True
	else:
		return False

## Function to identify an external declaration in a string and write out a C++ equivalent
# @param s a single line of source code 
# @return True if the member regex is not empty
# @return False otherwise
def foundDeclaration(s):
	global re_declaration
	s_func = re_declaration.match(strip_comments(s))
	if (s_func is not None):
		res_str = getAccessibility(s_func.group(1)) + " " + s_func.group(1) + " (" + s_func.group(3) + ";"
		outfile.write(res_str + "\n")
		return True
	else:
		return False

## Function to identify a subroutine in a string and write out a C++ equivalent
# @param s a single line of source code 
# @return True if the member regex is not empty
# @return False otherwise
def foundSub(s):
	global re_sub
	s_sub = re_sub.match(strip_comments(s))
	if (s_sub is not None):
		res_str = getAccessibility(s_sub.group(1)) + " Sub " + s_sub.group(3) + s_sub.group(4)  + ";"
		outfile.write(res_str + "\n")
		return True
	else:
		return False

## Function to return the C++ equivalent scope of a function or routine
# @param s a string to be evaluated
# @return the scope if found
# @return "public:" otherwise
def getAccessibility(s):
	accessibility = def_level
	if (s is not None):
		if (s.strip().lower() == "private"): accessibility = "private:"
		elif (s.strip().lower() == "public"): accessibility = "public:"
		elif (s.strip().lower() == "friend"): accessibility = "friend "
		elif (s.strip().lower() == "static"): accessibility = "static"
	return accessibility

## Method to write out a C++ equivalent for a user defined type or enum
# @param s a single line of source code 
def foundMemberOfType(s):
	global re_members
	global re_assignments
	member = re_members.match(strip_comments(s))
	assigns = re_assignments.match(strip_comments(s))
	if (member is not None):
		res_str = member.group(3) + " " + member.group(2)  + ";"
		outfile.write(res_str + "\n")
	elif (assigns is not None):
		res_str = assigns.group(1) + " = " + assigns.group(2)  + ","
		outfile.write(res_str + "\n")

## Function to identify a type or enum in a string and write out a C++ equivalent
# @param s a single line of source code 
# @return True if the member regex is not empty
# @return False otherwise
def foundType(s):
	global re_type
	vbType = re_type.match(strip_comments(s))
	if (vbType is not None):
		if (vbType.group(2) == "Enum"):
			res_str = getAccessibility(vbType.group(1)) + " enum " + vbType.group(3)  + " {"
		else:
			res_str = getAccessibility(vbType.group(1)) + " struct " + vbType.group(3)  + " {"
		outfile.write(res_str + "\n")
		return True
	else:
		return False

## Function to determine if we are still inside a Type or Enum definition
# @param s a line of source code
# @return False if "End Type or "End Enum" is not found
# @return True otherwise
def processType(s):
	global re_endType
	vbEndType = re_endType.match(strip_comments(s))
	if (vbEndType is not None):
		outfile.write("}; \n")
		return False
	else:
		foundMemberOfType(s)
		return True


## Strips a list of VB line continuations and concatenates them
# @param r a list of lines read from the input source file
# @return a list with all line continuations from the original list removed.
def joinLines (r):
	t = []
	u = ''
	for s in r:
		strip = s.rstrip(" _ \n").rstrip(" _\n")
		if s.rstrip(" \n") == strip:			
			u = u + ' ' + s.strip(" ")
			t.append(u)
			u = ''
		else:		
			u = u + strip
	return t

## Primary processing function.
# @param filename name of file to process
# @param type of file to process.  
#        Used to determine whether code is encapsulated in a class or a namespace.
def processFile(filename, type):
	global outfile
	f = open(filename)
	r = f.readlines()
	f.close()
	r = joinLines(r)

	# we have to look for global comments first (those that start with '!)
	processGlobalComments(r)
	findInheritedClasses(r)
	processClassName(r, type)
	inTypeSearch = False
	
	# For each line in the source file look for lines matching
	# indicated regular expressions
	for s in r:
		checkDoxyComment(s)
		if inTypeSearch:
			inTypeSearch = processType(s)
			continue	
		if foundType(s):
			inTypeSearch = True
			continue
		if foundMember(s):
			continue
		if foundConst(s):
			continue		
		if foundFunction(s):
			continue
		if foundSub(s):
			continue
		if foundDeclaration(s):
			continue
	outfile.write("}")  # for ending class


## VBFilter main function.
# 
#  This function decides whether the file is
#  - a bas file  - module
#  - a cls file  - class
#  - a frm file  - frame
#  and calls the appropriate function to process the file.
# @param filename name of file to process
# @param out filter output file.  Doxygen expect 'Input Filters' to write their
#        output to STDOUT
# @see http://www.stack.nl/~dimitri/doxygen/config.html#cfg_input_filter 
def filter(filename, out=sys.stdout):
	global outfile
	global verbose
	outfile = out
	try:
		root, ext = os.path.splitext(filename)
		if (ext.lower() ==".bas"):
			processFile(filename, "BAS")
		elif (ext.lower() ==".cls") or (ext.lower() == ".frm"):
			processFile(filename, "CLS")
		else:
			sys.stderr.write("Error: Unknown filename extension!\n") 
			dump(filename)

		if verbose:
			sys.stderr.write("OK\n") 
	except IOError,e:
		sys.stderr.write(e[1]+"\n")

## main-entry ##
################

if len(sys.argv) != 2:
	print "usage: ", sys.argv[0], " filename"
	sys.exit(1)

# Filter the specified file and print the result to stdout
filename = sys.argv[1] 
filter(filename)
sys.exit(0)
